//Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Text;

namespace Microsoft.Office.OneNote
{
	/// <summary>
	/// HierarchyNodes are high level nodes that can be independently synchronized
	/// in their entirety with any contained content.  
	/// Each HierarchyNode can be identified via its unique ObjectId, and maintains 
	/// state to facilitate implicit synchronization with the ApplicationServer.
	/// </summary>
	[Serializable]
	public abstract class HierarchyNode : ANode
	{
		/// <summary>
		/// Ensures our properties are in sync.
		/// </summary>
		public override void EnsurePropertiesSynchronized(bool force)
		{
			if (!IsConnected)
				return;

			if (!force)
			{
				// Throttle updates:
				if (this.SynchronizationLockedForWrite)
				{
					return;
				}

				TimeSpan deltaLastSynchronized = DateTime.Now.Subtract(PropertiesLastSynchronized);
				if (PropertiesLastSynchronized != DateTime.MinValue &&
					(deltaLastSynchronized.TotalMilliseconds < 1000 || this.SynchronizationLockedForRead))
				{
					return;
				}
			}

			// We need to synchronize our properties.  Get an updated branch of ourselves, and
			// synchronize ourselves against the branch.
			Type hierarchyType = this.GetType();
			HierarchyNode serverBranch = (HierarchyNode) Activator.CreateInstance(hierarchyType);
			using (HierarchySynchronizationLocker syncLock = new HierarchySynchronizationLocker(serverBranch, HierarchySynchronizationLocker.Mode.ForWrite))
			{
				serverBranch.DeserializeFromXml(GetHierarchyXmlFromServer(HierarchyScope.Self));

				this.Synchronize(serverBranch);
				this.CommitPending = false;
			}
		}

		/// <summary>
		/// Called after a property has been modified with the property, old value and new value.
		/// </summary>
		/// <param name="propertyName">The name of the modified property.</param>
		/// <param name="oldPropertyValue">The old value of the property.</param>
		/// <param name="newPropertyValue">The new value of the property.</param>
		protected override void OnPropertyModified(string propertyName, Object oldPropertyValue, Object newPropertyValue)
		{
			this.CommitPending = true;
		}

		/// <summary>
		/// Have we loaded our children yet?  Note that this doesn't guarantee that
		/// the children is in sync -- just that it's been loaded ever.
		/// </summary>
		/// <returns>True if this node has loaded our children.</returns>
		public override bool AreChildrenLoaded()
		{
			if (childrenLastSynchronized > DateTime.MinValue)
			{
				// We've synchronized our children once from the application server:
				return true;
			}
			else
			{
				// Are we user created content?
				if (children.Count > 0)
				{
					Debug.Assert(id.ToString().Length == 0);
					return true;
				}

				return false;
			}
		}

		/// <summary>
		/// Inserts the given node as a child at a specific position.  Updates the 
		/// tracked hierarchy state as well.
		/// </summary>
		/// <param name="child">The child node to be added.</param>
		/// <param name="index">The position where this child should be added.</param>
		/// <returns>The node inserted.</returns>
		public override ANode InsertChild(ANode child, int index)
		{
			child = base.InsertChild(child, index);

			// Update our hierarchy state:
			if (child is HierarchyNode)
				((HierarchyNode) child).CommitPending = true;

			return child;
		}

		/// <summary>
		/// Deletes the specified child, and updates tracked hierarchy state.
		/// </summary>
		/// <param name="child">The ANode to be removed.</param>
		public override void DeleteChild(ANode child)
		{
			if (child is HierarchyNode)
			{
				childNodesPendingDelete.Add((HierarchyNode) child);
				this.CommitPending = true;
			}

			base.DeleteChild(child);
		}

		/// <summary>
		/// Ensures our children are in sync.
		/// </summary>
		public override void EnsureChildrenSynchronized(bool deep, bool force)
		{
			if (!IsConnected)
				return;

			if (!force)
			{
				// Throttle updates:
				if (this.SynchronizationLockedForWrite)
				{
					return;
				}

				TimeSpan deltaLastSynchronized = DateTime.Now.Subtract(ChildrenLastSynchronized);
				if (AreChildrenLoaded() &&
					(deltaLastSynchronized.TotalMilliseconds < 1000 || this.SynchronizationLocked))
				{
					return;
				}
			}

			// We need to synchronize our children.  Get an updated branch of ourselves, and
			// synchronize ourselves against the branch.
			Type hierarchyType = this.GetType();
			HierarchyNode serverBranch = (HierarchyNode) Activator.CreateInstance(hierarchyType);
			using (HierarchySynchronizationLocker syncLock = new HierarchySynchronizationLocker(serverBranch, HierarchySynchronizationLocker.Mode.ForWrite))
			{
				HierarchyScope scope = deep ? HierarchyScope.Pages : HierarchyScope.Children;
				serverBranch.DeserializeFromXml(GetHierarchyXmlFromServer(scope));

				this.Synchronize(serverBranch);
				this.CommitPending = false;
			}
		}

		/// <summary>
		/// Deserializes the base properties shared by all <see cref="HierarchyNode"/> objects from 
		/// OneNote's xml representation. Should be chained in all overridden classes.
		/// </summary>
		/// <param name="node">
		/// A reference to the XML node that we should deserialize ourselves
		/// from.
		/// </param>
		protected internal override void DeserializeFromXml(XmlNode node)
		{
			XmlElement element = (XmlElement) node;

			string elementId = element.GetAttribute("ID");
			if (elementId != null && elementId.Length != 0)
				this.Id = new ObjectId(elementId);
			else
				this.Id = ObjectId.Empty;

			DateTime synchronized = DateTime.Now;
			PropertiesLastSynchronized = synchronized;

			if (HierarchyXmlHelper.NodeChildrenAreLoaded(element))
				ChildrenLastSynchronized = synchronized;
		}

		/// <summary>
		/// Queries the server for the node's children hierarchy.
		/// </summary>
		/// <param name="scope">The scope of the hierarchy to retrieve.</param>
		/// <returns>The serialized representation of this nodes Hierarchy.</returns>
		protected internal XmlElement GetHierarchyXmlFromServer(HierarchyScope scope)
		{
			string serialized;
			Application.Server.GetHierarchy(id.ToString(), scope, out serialized);

			// Parse XML:
			XmlDocument children = new XmlDocument();
			children.LoadXml(serialized);

			// Mark the boundaries of the loaded hierarchy...
			HierarchyXmlHelper.FlagUnloadedNodes(children.DocumentElement, scope);

			return children.DocumentElement;
		}

		/// <summary>
		/// Merges all common HierarchyNode properties from the given branch.
		/// </summary>
		/// <param name="branch">The node we're synchronizing ourself from.</param>
		public virtual void Synchronize(HierarchyNode branch)
		{
			Debug.Assert(this.CommitPending == false);
			Debug.Assert(this.Id.Equals(branch.Id));

			// Synchronize properties:
			this.properties.Clear();
			foreach (String key in branch.properties.Keys)
			{
				this.properties[key] = branch.properties[key];
			}
			this.PropertiesLastSynchronized = branch.PropertiesLastSynchronized;

			// Synchronize children:
			if (branch.AreChildrenLoaded())
			{
				Dictionary<ObjectId, HierarchyNode> mapChildren = new Dictionary<ObjectId, HierarchyNode>();
				foreach (ANode child in this.children)
				{
					HierarchyNode childHierarchyNode = child as HierarchyNode;
					if (childHierarchyNode == null)
					{
						// We're about to delete this node...
						Debug.Assert(false);
						continue;
					}

					mapChildren[childHierarchyNode.Id] = childHierarchyNode;
				}

				children.Clear();

				foreach (ANode branchChild in branch.children)
				{
					HierarchyNode branchChildHierarchyNode = branchChild as HierarchyNode;
					if (branchChildHierarchyNode == null)
					{
						children.Add(branchChild);
						branchChild.Parent = this;
						continue;
					}

					HierarchyNode child;
					if (mapChildren.TryGetValue(branchChildHierarchyNode.Id, out child))
					{
						child.Synchronize(branchChildHierarchyNode);
						children.Add(child);
					}
					else
					{
						children.Add(branchChildHierarchyNode);
						branchChildHierarchyNode.Parent = this;
					}
				}

				this.ChildrenLastSynchronized = branch.ChildrenLastSynchronized;
			}

			Debug.Assert(this.CommitPending == false);
		}

		/// <summary>
		/// Commits all changes to this hierarchy branch:
		/// </summary>
		public virtual void Commit()
		{
			if (this.CommitPending == false)
				return;

			// Handle any nodes pending deletion:
			CommitPendingDeletions();

			// Create any newly specified nodes:
			CommitPendingAdditions();

			// Serialize ourselves to XML:
			XmlDocument document = new XmlDocument();
			SerializePendingHierarchyChanges(document);

			// Save the serialized DOM to a text stream:
			MemoryStream xmlStream = new MemoryStream();

			XmlWriter xmlWriter = new XmlTextWriter(xmlStream, Encoding.Unicode);
			document.Save(xmlWriter);
			xmlWriter.Flush();

			// Convert the text stream to a string:
			xmlStream.Seek(0, SeekOrigin.Begin);
			string serializedDocument = new StreamReader(xmlStream).ReadToEnd();

			Application.Server.UpdateHierarchy(serializedDocument);

			this.CommitPending = false;
		}

		/// <summary>
		/// Recursively commits all nodes that have been marked for deletion:
		/// </summary>
		protected internal void CommitPendingDeletions()
		{
			foreach (HierarchyNode nodeToBeDeleted in childNodesPendingDelete)
			{
				if (nodeToBeDeleted.Id != ObjectId.Empty)
				{
					if (nodeToBeDeleted is Notebook)
						Application.Server.CloseNotebook(nodeToBeDeleted.Id.ToString());
					else
						Application.Server.DeleteHierarchy(nodeToBeDeleted.Id.ToString());
				}
			}
			childNodesPendingDelete.Clear();

			foreach (ANode child in children)
			{
				if (!(child is HierarchyNode))
					continue;

				HierarchyNode childNode = (HierarchyNode) child;
				if (childNode.CommitPending == true)
				{
					childNode.CommitPendingDeletions();
				}
			}
		}

		/// <summary>
		/// Recursively commits all newly created nodes:
		/// </summary>
		protected internal void CommitPendingAdditions()
		{
			foreach (ANode child in children)
			{
				if (!(child is HierarchyNode))
					continue;

				HierarchyNode childNode = (HierarchyNode) child;
				if (childNode.Id == ObjectId.Empty)
				{
					/* A new node!  Create it: */
					string serializedId = null;
					childNode.AddHierarchyToOneNote(out serializedId);

					if (serializedId != null && serializedId != ObjectId.Empty.ToString())
					{
						childNode.Id = new ObjectId(serializedId);
					}

					// Handle any newly created children:
					childNode.CommitPendingAdditions();
				}
				else if (childNode.CommitPending == true)
				{
					// Recurse:
					childNode.CommitPendingAdditions();
				}
			}
		}

		/// <summary>
		/// Used while committing all pending additions to commit any newly created
		/// node to OneNote.
		/// </summary>
		/// <param name="serializedId">The new ID of this node.</param>
		protected internal virtual void AddHierarchyToOneNote(out string serializedId)
		{
			Debug.Fail("Attempting to add a new non-supported hierarchy node.");
			serializedId = null;
		}

		/// <summary>
		/// Serializes all changes to the given hierarchy:
		/// </summary>
		/// <param name="parentNode">The parent XML node we should serialize ourselves to.</param>
		protected internal void SerializePendingHierarchyChanges(XmlNode parentNode)
		{
			XmlNode serialized = this.SerializeToXml(parentNode, false /* deep */);

			bool childrenModified = false;
			foreach (ANode child in children)
			{
				if (!(child is HierarchyNode))
					continue;

				HierarchyNode childNode = (HierarchyNode) child;
				if (childNode.CommitPending)
				{
					childrenModified = true;
					break;
				}
			}

			if (childrenModified)
			{
				// Serialize children hierarchy nodes:
				foreach (ANode child in children)
				{
					if (!(child is HierarchyNode))
						continue;

					HierarchyNode childNode = (HierarchyNode) child;
					childNode.SerializePendingHierarchyChanges(serialized);
				}
			}
		}

		/// <summary>
		/// The Unique ObjectID of the OneNote node object we're representing.
		/// </summary>
		public ObjectId Id
		{
			get
			{
				return id;
			}
			set
			{
				id = value;
			}
		}

		/// <summary>
		/// Our parent hierarchy node.
		/// </summary>
		public HierarchyNode ParentHierarchyNode
		{
			get
			{
				ANode current = this;
				while (current != null)
				{
					if (current.Parent is HierarchyNode)
					{
						return current.Parent as HierarchyNode;
					}

					current = current.Parent;
				}

				return null;
			}
		}

		/// <summary>
		/// The timestamp when our HierarchyNode properties were last updated to
		/// match it's peer object in OneNote.
		/// </summary>
		protected internal DateTime PropertiesLastSynchronized
		{
			get
			{
				return propertiesLastSynchronized;
			}
			set
			{
				propertiesLastSynchronized = value;
			}
		}

		/// <summary>
		/// The timestamp when our HierarchyNode children were last updated to
		/// match it's peer object in OneNote.
		/// </summary>
		protected internal DateTime ChildrenLastSynchronized
		{
			get
			{
				return childrenLastSynchronized;
			}
			set
			{
				childrenLastSynchronized = value;
			}
		}

		/// <summary>
		/// By definition, setting CommitPending to true invalidates all parent
		/// hierarchy nodes as well, and setting CommitPending to false validates all
		/// child hierarchy nodes.
		/// </summary>
		public bool CommitPending
		{
			get
			{
				return commitPending;
			}
			set
			{
				commitPending = value;

				if (commitPending)
				{
					Debug.Assert(!this.IsConnected || this.SynchronizationLockedForWrite);
					if (this.ParentHierarchyNode != null &&
						this.ParentHierarchyNode.SynchronizationLockedForWrite)
					{
						ParentHierarchyNode.CommitPending = true;
					}
				}
				else
				{
					for (int i = 0; i < children.Count; i++)
					{
						ANode child = (ANode) children[i];
						if (child is HierarchyNode)
							((HierarchyNode) child).CommitPending = false;
					}
				}
			}
		}

		/// <summary>
		/// Are we locked from synchronizing?
		/// </summary>
		public bool SynchronizationLocked
		{
			get
			{
				if (this.synchronizationLockCountForRead > 0 ||
					this.synchronizationLockCountForWrite > 0)
				{
					return true;
				}

				if (this.ParentHierarchyNode != null)
				{
					return this.ParentHierarchyNode.SynchronizationLocked;
				}

				return false;
			}
		}

		/// <summary>
		/// Are we locked from synchronizing for reading purposes?  If we're locked
		/// for read, we'll still load delay-loaded children.
		/// </summary>
		public bool SynchronizationLockedForRead
		{
			get
			{
				if (this.synchronizationLockCountForRead > 0)
					return true;

				if (this.ParentHierarchyNode != null)
				{
					return this.ParentHierarchyNode.SynchronizationLockedForRead;
				}

				return false;
			}
			set
			{
				this.synchronizationLockCountForRead += (value ? 1 : -1);

				Debug.Assert(synchronizationLockCountForRead > -1);
			}
		}

		/// <summary>
		/// Are we locked from synchronizing for write purposes?  If so, we won't load
		/// _anything_.
		/// </summary>
		public bool SynchronizationLockedForWrite
		{
			get
			{
				if (this.synchronizationLockCountForWrite > 0)
					return true;

				if (this.ParentHierarchyNode != null)
				{
					return this.ParentHierarchyNode.SynchronizationLockedForWrite;
				}

				return false;
			}
			set
			{
				this.synchronizationLockCountForWrite += (value ? 1 : -1);

				Debug.Assert(synchronizationLockCountForWrite > -1);
				Debug.Assert(synchronizationLockCountForWrite < 2);
			}
		}

		private ObjectId id = ObjectId.Empty;
		private bool commitPending;
		private DateTime propertiesLastSynchronized;
		private DateTime childrenLastSynchronized;
		private int synchronizationLockCountForRead;
		private int synchronizationLockCountForWrite;
		private List<HierarchyNode> childNodesPendingDelete = new List<HierarchyNode>();


		/// <summary>
		/// Helper class used when deserializing from XML.
		/// </summary>
		protected internal static class HierarchyXmlHelper
		{
			/// <summary>
			/// Helper method to check to see if the node's children are loaded.
			/// </summary>
			/// <param name="node">The node to check</param>
			/// <returns>true if the node children are loaded</returns>
			internal static bool NodeChildrenAreLoaded(XmlElement node)
			{
				if ("false".Equals(node.GetAttribute("childrenLoaded")))
					return false;

				return true;
			}

			/// <summary>
			/// Inserts special marker nodes within the specified XML hierarchy 
			/// tree as appropriate to denote the boundary of our loaded hierarchy.
			/// </summary>
			/// <param name="node">The node to be processed</param>
			/// <param name="scope">The scope of the loaded hierarchy</param>
			internal static void FlagUnloadedNodes(XmlElement node, HierarchyScope scope)
			{
				if (IsNodeOfScope(node, scope))
				{
					FlagUnloadedNode(node);
					return;
				}

				// If we're a page, then we're done:
				if (node.LocalName.Equals("Page"))
					return;

				// Recurse:
				if (scope == HierarchyScope.Children)
					scope = HierarchyScope.Self;

				foreach (XmlElement child in node.ChildNodes)
				{
					XmlElement childElement = child as XmlElement;
					if (childElement != null)
						FlagUnloadedNodes(childElement, scope);
				}
			}

			/// <summary>
			/// Compares the given XmlElement to the specified HierarchyScope and 
			/// returns true if they match.
			/// </summary>
			/// <param name="node">The node to compare.</param>
			/// <param name="scope">The scope we're matching against.</param>
			/// <returns>true if the node represents the given HierarchyScope</returns>
			internal static bool IsNodeOfScope(XmlElement node, HierarchyScope scope)
			{
				if (node.LocalName == "Notebook" && scope == HierarchyScope.Notebooks)
					return true;
				if (node.LocalName == "Sections" && scope == HierarchyScope.Sections)
					return true;
				if (node.LocalName == "Page" && scope == HierarchyScope.Pages)
					return true;

				if (scope == HierarchyScope.Self)
					return true;

				return false;
			}

			/// <summary>
			/// Flags the given XmlElement as not having any loaded children.
			/// </summary>
			/// <param name="node">The XmlElement to flag.</param>
			internal static void FlagUnloadedNode(XmlElement node)
			{
				Debug.Assert(node.ChildNodes.Count == 0);
				node.SetAttribute("childrenLoaded", "false");
			}
		}
	}
}